/*
 * Copyright (C) 2009 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.collect.testing.google;

import static com.google.common.collect.testing.features.CollectionFeature.ALLOWS_NULL_VALUES;
import static com.google.common.collect.testing.features.CollectionFeature.FAILS_FAST_ON_CONCURRENT_MODIFICATION;
import static com.google.common.collect.testing.features.CollectionFeature.RESTRICTS_ELEMENTS;
import static com.google.common.collect.testing.features.CollectionFeature.SUPPORTS_ADD;
import static com.google.common.collect.testing.features.CollectionFeature.SUPPORTS_REMOVE;
import static com.google.common.collect.testing.features.CollectionSize.SEVERAL;
import static com.google.common.collect.testing.features.CollectionSize.ZERO;

import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.collect.Multiset;
import com.google.common.collect.Multiset.Entry;
import com.google.common.collect.testing.Helpers;
import com.google.common.collect.testing.features.CollectionFeature;
import com.google.common.collect.testing.features.CollectionSize;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;

import org.junit.Ignore;

/**
 * Common superclass for {@link MultisetSetCountUnconditionallyTester} and {@link
 * MultisetSetCountConditionallyTester}. It is used by those testers to test calls to the
 * unconditional {@code setCount()} method and calls to the conditional {@code setCount()} method
 * when the expected present count is correct.
 *
 * @author Chris Povirk
 */
@GwtCompatible(emulated = true)
@Ignore // Affects only Android test runner, which respects JUnit 4 annotations on JUnit 3 tests.
public abstract class AbstractMultisetSetCountTester<E> extends AbstractMultisetTester<E>
{
    /*
     * TODO: consider adding MultisetFeatures.SUPPORTS_SET_COUNT. Currently we
     * assume that using setCount() to increase the count is permitted iff add()
     * is permitted and similarly for decrease/remove(). We assume that a
     * setCount() no-op is permitted if either add() or remove() is permitted,
     * though we also allow it to "succeed" if neither is permitted.
     */

    private void assertSetCount(E element, int count)
    {
        setCountCheckReturnValue(element, count);

        assertEquals(
                "multiset.count() should return the value passed to setCount()",
                count,
                getMultiset().count(element));

        int size = 0;
        for (Multiset.Entry<E> entry : getMultiset().entrySet())
        {
            size += entry.getCount();
        }
        assertEquals(
                "multiset.size() should be the sum of the counts of all entries",
                size,
                getMultiset().size());
    }

    /**
     * Call the {@code setCount()} method under test, and check its return value.
     */
    abstract void setCountCheckReturnValue(E element, int count);

    /**
     * Call the {@code setCount()} method under test, but do not check its return value. Callers
     * should use this method over {@link #setCountCheckReturnValue(Object, int)} when they expect
     * {@code setCount()} to throw an exception, as checking the return value could produce an
     * incorrect error message like "setCount() should return the original count" instead of the
     * message passed to a later invocation of {@code fail()}, like "setCount should throw
     * UnsupportedOperationException."
     */
    abstract void setCountNoCheckReturnValue(E element, int count);

    private void assertSetCountIncreasingFailure(E element, int count)
    {
        try
        {
            setCountNoCheckReturnValue(element, count);
            fail("a call to multiset.setCount() to increase an element's count should throw");
        }
        catch (UnsupportedOperationException expected)
        {
        }
    }

    private void assertSetCountDecreasingFailure(E element, int count)
    {
        try
        {
            setCountNoCheckReturnValue(element, count);
            fail("a call to multiset.setCount() to decrease an element's count should throw");
        }
        catch (UnsupportedOperationException expected)
        {
        }
    }

    // Unconditional setCount no-ops.

    private void assertZeroToZero()
    {
        assertSetCount(e3(), 0);
    }

    private void assertOneToOne()
    {
        assertSetCount(e0(), 1);
    }

    private void assertThreeToThree()
    {
        initThreeCopies();
        assertSetCount(e0(), 3);
    }

    @CollectionFeature.Require(SUPPORTS_ADD)
    public void testSetCount_zeroToZero_addSupported()
    {
        assertZeroToZero();
    }

    @CollectionFeature.Require(SUPPORTS_REMOVE)
    public void testSetCount_zeroToZero_removeSupported()
    {
        assertZeroToZero();
    }

    @CollectionFeature.Require(absent = {SUPPORTS_ADD, SUPPORTS_REMOVE})
    public void testSetCount_zeroToZero_unsupported()
    {
        try
        {
            assertZeroToZero();
        }
        catch (UnsupportedOperationException tolerated)
        {
        }
    }

    @CollectionSize.Require(absent = ZERO)
    @CollectionFeature.Require(SUPPORTS_ADD)
    public void testSetCount_oneToOne_addSupported()
    {
        assertOneToOne();
    }

    @CollectionSize.Require(absent = ZERO)
    @CollectionFeature.Require(SUPPORTS_REMOVE)
    public void testSetCount_oneToOne_removeSupported()
    {
        assertOneToOne();
    }

    @CollectionSize.Require(absent = ZERO)
    @CollectionFeature.Require(absent = {SUPPORTS_ADD, SUPPORTS_REMOVE})
    public void testSetCount_oneToOne_unsupported()
    {
        try
        {
            assertOneToOne();
        }
        catch (UnsupportedOperationException tolerated)
        {
        }
    }

    @CollectionSize.Require(SEVERAL)
    @CollectionFeature.Require(SUPPORTS_ADD)
    public void testSetCount_threeToThree_addSupported()
    {
        assertThreeToThree();
    }

    @CollectionSize.Require(SEVERAL)
    @CollectionFeature.Require(SUPPORTS_REMOVE)
    public void testSetCount_threeToThree_removeSupported()
    {
        assertThreeToThree();
    }

    @CollectionSize.Require(SEVERAL)
    @CollectionFeature.Require(absent = {SUPPORTS_ADD, SUPPORTS_REMOVE})
    public void testSetCount_threeToThree_unsupported()
    {
        try
        {
            assertThreeToThree();
        }
        catch (UnsupportedOperationException tolerated)
        {
        }
    }

    // Unconditional setCount size increases:

    @CollectionFeature.Require(SUPPORTS_ADD)
    public void testSetCount_zeroToOne_supported()
    {
        assertSetCount(e3(), 1);
    }

    @CollectionFeature.Require({SUPPORTS_ADD, FAILS_FAST_ON_CONCURRENT_MODIFICATION})
    public void testSetCountZeroToOneConcurrentWithIteration()
    {
        try
        {
            Iterator<E> iterator = collection.iterator();
            assertSetCount(e3(), 1);
            iterator.next();
            fail("Expected ConcurrentModificationException");
        }
        catch (ConcurrentModificationException expected)
        {
            // success
        }
    }

    @CollectionFeature.Require({SUPPORTS_ADD, FAILS_FAST_ON_CONCURRENT_MODIFICATION})
    public void testSetCountZeroToOneConcurrentWithEntrySetIteration()
    {
        try
        {
            Iterator<Entry<E>> iterator = getMultiset().entrySet().iterator();
            assertSetCount(e3(), 1);
            iterator.next();
            fail("Expected ConcurrentModificationException");
        }
        catch (ConcurrentModificationException expected)
        {
            // success
        }
    }

    @CollectionFeature.Require(SUPPORTS_ADD)
    public void testSetCount_zeroToThree_supported()
    {
        assertSetCount(e3(), 3);
    }

    @CollectionSize.Require(absent = ZERO)
    @CollectionFeature.Require(SUPPORTS_ADD)
    public void testSetCount_oneToThree_supported()
    {
        assertSetCount(e0(), 3);
    }

    @CollectionFeature.Require(absent = SUPPORTS_ADD)
    public void testSetCount_zeroToOne_unsupported()
    {
        assertSetCountIncreasingFailure(e3(), 1);
    }

    @CollectionFeature.Require(absent = SUPPORTS_ADD)
    public void testSetCount_zeroToThree_unsupported()
    {
        assertSetCountIncreasingFailure(e3(), 3);
    }

    @CollectionSize.Require(absent = ZERO)
    @CollectionFeature.Require(absent = SUPPORTS_ADD)
    public void testSetCount_oneToThree_unsupported()
    {
        assertSetCountIncreasingFailure(e3(), 3);
    }

    // Unconditional setCount size decreases:

    @CollectionSize.Require(absent = ZERO)
    @CollectionFeature.Require(SUPPORTS_REMOVE)
    public void testSetCount_oneToZero_supported()
    {
        assertSetCount(e0(), 0);
    }

    @CollectionFeature.Require({SUPPORTS_REMOVE, FAILS_FAST_ON_CONCURRENT_MODIFICATION})
    @CollectionSize.Require(absent = ZERO)
    public void testSetCountOneToZeroConcurrentWithIteration()
    {
        try
        {
            Iterator<E> iterator = collection.iterator();
            assertSetCount(e0(), 0);
            iterator.next();
            fail("Expected ConcurrentModificationException");
        }
        catch (ConcurrentModificationException expected)
        {
            // success
        }
    }

    @CollectionFeature.Require({SUPPORTS_REMOVE, FAILS_FAST_ON_CONCURRENT_MODIFICATION})
    @CollectionSize.Require(absent = ZERO)
    public void testSetCountOneToZeroConcurrentWithEntrySetIteration()
    {
        try
        {
            Iterator<Entry<E>> iterator = getMultiset().entrySet().iterator();
            assertSetCount(e0(), 0);
            iterator.next();
            fail("Expected ConcurrentModificationException");
        }
        catch (ConcurrentModificationException expected)
        {
            // success
        }
    }

    @CollectionSize.Require(SEVERAL)
    @CollectionFeature.Require(SUPPORTS_REMOVE)
    public void testSetCount_threeToZero_supported()
    {
        initThreeCopies();
        assertSetCount(e0(), 0);
    }

    @CollectionSize.Require(SEVERAL)
    @CollectionFeature.Require(SUPPORTS_REMOVE)
    public void testSetCount_threeToOne_supported()
    {
        initThreeCopies();
        assertSetCount(e0(), 1);
    }

    @CollectionSize.Require(absent = ZERO)
    @CollectionFeature.Require(absent = SUPPORTS_REMOVE)
    public void testSetCount_oneToZero_unsupported()
    {
        assertSetCountDecreasingFailure(e0(), 0);
    }

    @CollectionSize.Require(SEVERAL)
    @CollectionFeature.Require(absent = SUPPORTS_REMOVE)
    public void testSetCount_threeToZero_unsupported()
    {
        initThreeCopies();
        assertSetCountDecreasingFailure(e0(), 0);
    }

    @CollectionSize.Require(SEVERAL)
    @CollectionFeature.Require(absent = SUPPORTS_REMOVE)
    public void testSetCount_threeToOne_unsupported()
    {
        initThreeCopies();
        assertSetCountDecreasingFailure(e0(), 1);
    }

    // setCount with nulls:

    @CollectionSize.Require(absent = ZERO)
    @CollectionFeature.Require({SUPPORTS_REMOVE, ALLOWS_NULL_VALUES})
    public void testSetCount_removeNull_nullSupported()
    {
        initCollectionWithNullElement();
        assertSetCount(null, 0);
    }

    @CollectionFeature.Require(
            value = {SUPPORTS_ADD, ALLOWS_NULL_VALUES},
            absent = RESTRICTS_ELEMENTS)
    public void testSetCount_addNull_nullSupported()
    {
        assertSetCount(null, 1);
    }

    @CollectionFeature.Require(value = SUPPORTS_ADD, absent = ALLOWS_NULL_VALUES)
    public void testSetCount_addNull_nullUnsupported()
    {
        try
        {
            setCountNoCheckReturnValue(null, 1);
            fail("adding null with setCount() should throw NullPointerException");
        }
        catch (NullPointerException expected)
        {
        }
    }

    @CollectionFeature.Require(ALLOWS_NULL_VALUES)
    public void testSetCount_noOpNull_nullSupported()
    {
        try
        {
            assertSetCount(null, 0);
        }
        catch (UnsupportedOperationException tolerated)
        {
        }
    }

    @CollectionFeature.Require(absent = ALLOWS_NULL_VALUES)
    public void testSetCount_noOpNull_nullUnsupported()
    {
        try
        {
            assertSetCount(null, 0);
        }
        catch (NullPointerException | UnsupportedOperationException tolerated)
        {
        }
    }

    @CollectionSize.Require(absent = ZERO)
    @CollectionFeature.Require(ALLOWS_NULL_VALUES)
    public void testSetCount_existingNoNopNull_nullSupported()
    {
        initCollectionWithNullElement();
        try
        {
            assertSetCount(null, 1);
        }
        catch (UnsupportedOperationException tolerated)
        {
        }
    }

    // Negative count.

    @CollectionFeature.Require(SUPPORTS_REMOVE)
    public void testSetCount_negative_removeSupported()
    {
        try
        {
            setCountNoCheckReturnValue(e3(), -1);
            fail("calling setCount() with a negative count should throw IllegalArgumentException");
        }
        catch (IllegalArgumentException expected)
        {
        }
    }

    @CollectionFeature.Require(absent = SUPPORTS_REMOVE)
    public void testSetCount_negative_removeUnsupported()
    {
        try
        {
            setCountNoCheckReturnValue(e3(), -1);
            fail(
                    "calling setCount() with a negative count should throw "
                            + "IllegalArgumentException or UnsupportedOperationException");
        }
        catch (IllegalArgumentException | UnsupportedOperationException expected)
        {
        }
    }

    // TODO: test adding element of wrong type

    /**
     * Returns {@link Method} instances for the {@code setCount()} tests that assume multisets support
     * duplicates so that the test of {@code Multisets.forSet()} can suppress them.
     */
    @GwtIncompatible // reflection
    public static List<Method> getSetCountDuplicateInitializingMethods()
    {
        return Arrays.asList(
                getMethod("testSetCount_threeToThree_removeSupported"),
                getMethod("testSetCount_threeToZero_supported"),
                getMethod("testSetCount_threeToOne_supported"));
    }

    @GwtIncompatible // reflection
    private static Method getMethod(String methodName)
    {
        return Helpers.getMethod(AbstractMultisetSetCountTester.class, methodName);
    }
}
